江湖上總有個傳言,說函數式編程的神人不需要 if 跟 else,只要 map, filter, reduce 就可以把事情都做完了。我覺得這個傳言該是要有中文版的澄清的時刻了。事實是,當語言有了 pattern matching 跟 guard,配合那些高階函式,就足以解決絕大部份的情況了。跟神不神人一點關係也沒有。
但是程式基本上是寫給人看的,為了要清楚表達我們的意圖,適當的使用 if else 跟 case,可以讓讀人的更明白:「嘿這裡有個分支條件,不是這樣,就是那樣。」或是「接下來的這段程式,我們要靠這個值來決定要做這些函式的其中一個。」。
Elixir 裡,就提供了 if...else
,case
及 cond
三種語法:
if...else
,但是沒有 elseifif...else
的語法長這個樣子:
if term do
# 當 term 為真值
else
# 當 term 為假值
end
跟 Ruby 一樣,當 term
為 false
或是 nil
時,才會跳過 if 的區塊,執行 else 區塊,其它情況皆視為真值。當然 term
也可以是個表達式,例如 ==
判斷之類的。
由於 if...else
本身也是個會回傳值的表達式,所以你可以用它來進行變數綁定:
a =
if 0 == 1 do
100
else
200
end
#=> a = 200
Elixir 裡沒有 Ruby 跟 JavaScript 裡的三元判斷式。如果你需要短一點的寫法,你可以用上之前說過的 do:
縮寫:
a = if 0 == 1, do: 100, else: 200
if
用來表達只有某個條件符合才執行,if...else
用於表達二擇一的情況,沒有其它語言中常有的 elseif 或類似的東西。當情況有三種以上,你要改用 case
。
Note: 其實當 Elixir 運作時,是把 if...else
轉成只有兩種分支的 case
來執行的。
unless
這顯然也是跟 Ruby 抄來致敬的。官方有建議不要在 unless
裡面放 else
區塊,因為這樣腦袋不太容易轉。 unless
是用來表示當某個條件不符合的時候,才執行該區塊:
unless 1 == 2 do
"這是個正常的世界"
end
case
case
的語法如下:
case term do
args -> do_something
end
是不是很像之前提過的函名函式?沒錯,就像匿名函式一樣,在 case 裡, 也可以使用 pattern matching 及 guards:
case {1, 2, 3} do
{4, 5, 6} ->
"這個不會執行"
{z, 2, 3} when z > 100->
"也不會執行, 第一個元素沒有大於 100"
{1, x, 3} ->
"這裡會把 x 綁定成 2"
_ ->
"如果沒有比對到,就會回傳這個"
end
首先注意一下最下面那個區塊。當 case 試著比對 term
失敗時,會拋出錯誤。因此慣例上會在最底下放一個 _ -> something
區塊,來比對所有其它的情況。還記得之前說過 _
這個我什麼都不在乎的語法吧?
cond
if
跟 case
都針對一個表達式進行判斷,如果你今天要處理的是多個條件的判斷,那就可以使用 cond
:
a = 1; b = 2; c = 3
cond do
a == 10 && b == 5 -> 100
b == 10 || c == 3 -> 200
true -> 500
end
由於 cond
是判斷每個區塊箭號左手側的值是否是真值,所以它的 default 區塊要改用 true ->
來接住所有的其它情況,當然慣例上都會加這個區塊以防止錯誤發生。
cond
當然也是個表達式,可以用變數去接它求出來的值。
for
許多人在聽說 elixir 裡面沒有 for
迴圈語法,會有兩個階段的反應。一開始會覺得「那我要怎麼遍歷一個集合啊?」這一點你現在就可以回答了:用高階函式去轉換集合的形狀更加好用。非得要表達一步步執行,而且 Enum.map/2
跟 Enum.reduce/3
都不符所需的情況下,還是有 Enum.each/2
這個函式。
再過一陣子就會有人發現,明明文件裡還是有一個 for
啊!但是這個 for
跟迴圈沒有關係,他完整的名字叫做 list comprehension。這是用來給定一個起始範圍及生成條件,用來生成另一個串列的語法。可以說是 Enum.map/2
加上 Enum.filter/2
的語法糖。
先看一下示範:
for n <- [1, 2, 3], do: n * n
#=> [1, 4, 9]
for n <- 1..4, rem(n, 2) == 0, do: n * n
#=> [4, 16]
在第一個例子裡,可以看到 <-
右側是起始的串列,然後在 do
區塊裡,寫的是如何產生新的值的表達式。
而在第二個例子裡,在初始串列及 do
區塊之間,我們加了一個叫 filter 的表達式,用來判斷什麼樣的值才會被納入。另外 <-
右側可以接受 1..4
這種叫 range 的語法。
只要增加上述的規則,就可以做出更複雜的串列,甚至可以模擬巢狀迴圈產生的結果,例如:
for x <- 1..3, y <- 4..6, do: {x, y}
#=> [{1, 4}, {1, 5}, {1, 6}, {2, 4}, {2, 5}, {2, 6}, {3, 4}, {3, 5}, {3, 6}]
如果你看到有人拿 for
來處理迴圈,其實八成也能做出想要的效果,但是你要知道這原本不是設計來這樣用的。
在條件裡加上 into:
選項,還可以做出其它的集合:
for k <- [:a, :b, :c], v <- [3], into: %{}, do: {k, v * v}
#=> %{a: 9, b: 9, c: 9}
一樣是 list comprehension,在 Erlang 裡是這樣寫的:
[X || X <- [1, 2, 3, 4], X > 2].
這是我個人少數偏好 Erlang 語法的情況。因為對比數學上的 set notation:
就很能了解這個語法當初是怎麼發想的。
這是個遞迴裡很有名的題目,西洋棋的皇后可以吃掉它所在的整行,整列及兩條斜線上的棋。我們想知道在 n * n 的棋盤裡,如何放進 n 個西洋棋的皇后,且它們彼此是不互相攻擊的。寫一段程式,輸入 n ,就計算出 n 個皇后在棋盤中位置的排列組合。
猜一下需要幾行?
Erlang 裡有個著名的四行解:
rows(0) -> [[]];
rows(N) ->
[[Pos | Columns] || Columns <- rows(N-1),
Pos <- [1,2,3,4,5,6,7,8] -- Columns,
save(Pos, Columns, 1)].
save(_Pos, [], _N) -> true;
save(Pos, [Column | Columns], N) ->
Pos /= Column + N andalso Pos /= Column - N andalso
save(Pos, Columns, N + 1).
在 Elixir 裡,因為語法的限制,可以用完全相同的邏輯及功能,用少於十行實作出來。你已經有所有需要的工具了,試試看吧?(提示: Erlang 裡的 /=
是 Elixir 裡的 !=
)
if
跟 unless
用來表達條件符合/不符合時才執行的情況if…else
用來表達二擇一的情況case
case
可以用 pattern matching 及 guardscase
最後建議放個 _ ->
預設比對區塊cond
用於複雜條件判斷cond
的預設比對區塊要用 true ->
for
不是迴圈,是生成串列(或其它集合)用的說要輕鬆寫,結果連八皇后都出現了。明天要來解釋 struct 跟 sigil。
Happy hacking! 明天見。